package com.google.code.doxiafiletreetool;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.io.FileUtils;
import org.apache.maven.doxia.Converter;
import org.apache.maven.doxia.ConverterException;
import org.apache.maven.doxia.DefaultConverter;
import org.apache.maven.doxia.UnsupportedFormatException;
import org.apache.maven.doxia.module.markdown.MarkdownParser;
import org.apache.maven.doxia.module.markdown.MarkdownSinkFactory;
import org.apache.maven.doxia.parser.ParseException;
import org.apache.maven.doxia.sink.Sink;
import org.apache.maven.doxia.wrapper.InputFileWrapper;
import org.apache.maven.doxia.wrapper.OutputFileWrapper;
import org.codehaus.plexus.util.ReaderFactory;


/**
 * Main implementation class and main entry point.
 * TODO: ignore "target" dir
 */
public class Main {
    
    private static final Converter converter = new DefaultConverter();
    
    private static final Map<String, FileFilter> filters = new Hashtable<String, FileFilter>();
    
    private static FileFilter defaultDirFilter = null;
    
    private static Calendar now = Calendar.getInstance();
    
    private static String currentYear = Integer.toString(now.get(Calendar.YEAR));

    private static String currentMonth = Integer.toString(now.get(Calendar.MONTH) + 1);
    
    private static String currentDay = Integer.toString(now.get(Calendar.DAY_OF_MONTH));
    
    private static Pattern semanticDefinition = Pattern.compile("%%.*?%%");
    
    private static Pattern semanticReference = Pattern.compile("%!?.*?%");
    
    private static Map<String, String> patterns = new Hashtable<String, String>();
    
    private static Map<String, AtomicInteger> counts = new Hashtable<String, AtomicInteger>(); 
    
    
    /**
     * Filter for directories.
     */
    private static final FileFilter createDirFilter(final Set<String> ignoreNames) {
        if ((defaultDirFilter == null) || ((ignoreNames != null) && (ignoreNames.size() > 0))) {
            @SuppressWarnings("serial")
            final Set<String> ignore = new HashSet<String>() {{
                add(Constants.svnFolder);
                add(Constants.target);
                if (ignoreNames != null) {
                    addAll(ignoreNames);
                }
            }};
            final FileFilter fileFilter = new FileFilter() {
                @Override
                public boolean accept(final File entry) {
                    return !Constants.dotPattern.matcher(entry.getName()).matches() && entry.isDirectory() && entry.canRead() && !ignore.contains(entry.getName());
                }
            };
            if ((defaultDirFilter == null) && (ignoreNames != null) && (ignoreNames.size() == 0)) {
                defaultDirFilter = fileFilter;
            }
            return fileFilter;
        }
        return defaultDirFilter;
    }
    
    
    /**
     * Get one file extension filter lazily.
     * @param extension The extension to filter
     * @return The filter
     */
    private synchronized static final FileFilter getFileFilter(final String extension) {
        FileFilter ff = filters.get(extension);
        if (ff == null) {
            ff = new FileFilter() {
                @Override
                public boolean accept(final File entry) {
                    return !Constants.dotPattern.matcher(entry.getName()).matches() && entry.getName().toLowerCase().endsWith(extension) && entry.isFile() && entry.canRead();
                }
            };
            filters.put(extension, ff);
        }
        return ff;
    };
    

    /**
     * Main entry point.
     * @param args Command line arguments. Path = path to start (else current), -[-]combine
     */
    public static void main(final String... args) {
//    	final Log log = new SystemStreamLog(); // logger disturbs the flushed output
//    	log.setLogLevel(Log.LEVEL_WARN); // this is default anyway
//        converter.enableLogging(log);
//        converter.setFormatOutput(true);
        final File startDir;
        final File targetDir;
        if (args.length >= 1) {
            startDir = new File(args[0]);
        } else {
            startDir = new File(Constants.dot);
        }
        if (args.length >= 2) {
            targetDir = new File(args[1]);
        } else {
            targetDir = new File(startDir, Constants.target);
        }
        new Main().run(startDir, targetDir, true);
    }
    
    /**
     * @param dir The dir to start in
     * @param combine If true then combine all html files into one (per directory)
     */
    public void run(final File dir, final File targetDir, final boolean combine) {
        assert(dir != null);
        assert(targetDir != null);
        
        if (!targetDir.exists()) {
            if (!targetDir.mkdirs()) {
                System.err.println("Could not create target directory");
                return;
            }
        }
        try {
            FileUtils.copyURLToFile(getClass().getResource("/freeplaneviewer.jar"), new File(targetDir, "freeplaneviewer.jar"));
            FileUtils.copyURLToFile(getClass().getResource("/favicon.ico"), new File(targetDir, "favicon.ico"));
        } catch (final IOException e) {
            e.printStackTrace();
            return;
        }
        try {
            recurseIntoDir(dir, targetDir, combine, 0);
        } catch (final FileNotFoundException e) {
            e.printStackTrace();
        }
    }
    
    private static boolean createDir(final File targetDir) {
        if (!targetDir.exists()) {
            if (!targetDir.mkdirs()) {
                System.err.println("Could not create target directory, skipping further recursion " + targetDir.getAbsolutePath());
                return false; // special case: marking "nothing was changed", but is actually an error
            }
        }
        return true;
    }
    
    /**
     * Recursive directory walker without circle detection.
     * @param current The current directory to start processing files in
     * @param targetDir the target directory in current recursion depth
     * @param combine If true then combine all html files into one (per directory)
     * @param depth Current recursion depth, starting with 0.
     * @return true if something was created/copied, false if nothing was done
     * @throws FileNotFoundException 
     */
    private static boolean recurseIntoDir(final File current, final File targetDir, final boolean combine, final int depth) throws FileNotFoundException {
        System.out.println("[" + current.getPath() + "]");
        // process doxia files into xhtml files
        final File[] markdown = current.listFiles(getFileFilter(Constants.markdownExtension));
        final File[] apt = current.listFiles(getFileFilter(Constants.aptExtension));
        final File[] files = new File[markdown.length + apt.length];
        System.arraycopy(markdown, 0, files, 0, markdown.length);
        System.arraycopy(apt, 0, files, markdown.length, apt.length); // aggregate file names
        boolean createdSomething = false;
        if (files.length > 0) {
            createdSomething = true;
            if (!createDir(targetDir)) {
                return false;
            }
            for (final File file: files) {
                processFile(file, targetDir);
            }
        }
        final File[] mindMaps = current.listFiles(getFileFilter(Constants.mmExtension));
        // copy linked contents of [x]htm[l] files (Windows- and Browser-feature)
        final Set<String> ignoreDirs = new HashSet<String>();
        final File[] htmlFiles = current.listFiles(Constants.htmlFilter);
        if (htmlFiles.length > 0) {
            createdSomething = true;
            if (!createDir(targetDir)) {
                return false;
            }
            for (final File file: htmlFiles) {
                final String name = file.getName().substring(0, file.getName().lastIndexOf(Constants.dot)) + Constants.linkedDir;
                final File dir = new File(current, name);
                if (dir.exists()) { // linked directory exists: ignore it for recursion consideration
                    ignoreDirs.add(name);
                    final File destDir = new File(targetDir, name);
                    if (destDir.mkdir()) { // but copy its entire contents to target
                        try {
                            FileUtils.copyDirectory(dir, destDir);
                        } catch (final IOException e) {
                        }
                    }
                }
            }
        }
        // recurse into subdirectories
        final File[] dirs = current.listFiles(createDirFilter(ignoreDirs));
        for (final File dir: dirs) {
            createdSomething |= recurseIntoDir(dir, new File(targetDir, dir.getName()), combine, depth + 1);
        }
        // create index
        if ((mindMaps.length == 0) && !createdSomething) {
            return false; // nothing was changed in subfolders, so no index was created and no directory created
        }
        if (!createDir(targetDir)) {
            return false;
        }
        // for mindmap viewer and favicon
        String relativePath = "";
        for (int i = 1; i <= depth; i ++) {
            relativePath += "../";
        }
        final OutputStream fos = new FileOutputStream(new File(targetDir, Constants.indexHtml));
        try {
            fos.write(UnicodeUtil.convert(Constants.header.replace("%FAVPATH%", relativePath).getBytes(), Constants.utf8));
            fos.write(UnicodeUtil.convert(("<h1>[" + current.getName() + "]</h1>\n<a href=\"../index.xhtml\">Parent file list</a>").getBytes(), Constants.utf8));
            if (dirs.length > 0) { // Folder links
                fos.write(UnicodeUtil.convert((("<h3>Folders</h3>\n").getBytes()), Constants.utf8));
                for (final File dir: dirs) {
                    if (dir.getName().equals(Constants.svnFolder) || dir.getName().equals(Constants.target)) {
                        continue;
                    }
                    fos.write(UnicodeUtil.convert(("<a href=\"" + dir.getName() + "/" + Constants.indexHtml + "\">[" + dir.getName() + "]</a>\n").getBytes(), Constants.utf8));
                }
            }
            if (files.length > 0) { // converted APT file links
                final Map<String, File> map = new Hashtable<String, File>();
                final String[] names = new String[files.length];
                int i = 0;
                for (final File file: files) {
                    map.put(file.getName(), file);
                    names[i ++] = file.getName();
                }
                Arrays.sort(names);
                String last = " ";
                String write = "";
                fos.write(UnicodeUtil.convert(Constants.titleDocuments, Constants.utf8));
                for (final String n: names) { // first pass: filter date-files
                    final File file = map.get(n);
                    final String name = file.getName().substring(0, file.getName().lastIndexOf(Constants.dot));
                    final String next;
                    if ((name.length() >= 8) && name.substring(0, 8).matches("[0-9]{8}")) {
                        if (name.substring(0, 4).equals(currentYear) && (last.length() >= 4) && name.substring(0, 4).equals(last.substring(0, 4))) {
                            if (name.substring(4, 6).equals(currentMonth) && (last.length() >= 6) && name.substring(4, 6).equals(last.substring(0, 6))) {
                                if (name.substring(6, 8).equals(currentDay) && (last.length() >= 8) && name.substring(6, 8).equals(last.substring(0, 8))) {
                                    next = getFirstAlphaChar(name).toUpperCase();
                                    write = next; 
                                } else {
                                    next = name.substring(0, 8);
                                    write = "Day " + name.subSequence(6, 8);
                                }
                            } else {
                                next = name.substring(0, 6);
                                write = "Month " + name.substring(4, 6); 
                            }
                        } else {
                            next = name.substring(0, 4);
                            write = "Year " + next;
                        }
                    } else {
                        next = getFirstAlphaChar(name).toUpperCase();
                        write = next;
                    }
                    if (!next.equals(last)) {
                        last = next;
                        fos.write(UnicodeUtil.convert(("<span style=\"font-size:large;font-weight:bold\">" + write + "</span>&nbsp;").getBytes(), Constants.utf8));
                    }
                    fos.write(UnicodeUtil.convert(("<a href=\"" + name + Constants.xhtmlExtension + "\">" + name + "</a>\n").getBytes(), Constants.utf8));
                }
            }
            if (htmlFiles.length > 0) {
                fos.write(UnicodeUtil.convert(Constants.titleHtml, Constants.utf8));
                for (final File file: htmlFiles) {
                    final String name = file.getName().substring(0, file.getName().lastIndexOf(Constants.dot));
                    fos.write(UnicodeUtil.convert(("<a href=\"" + file.getName() + "\">" + name + "</a>\n").getBytes(), Constants.utf8));
                    FileUtils.copyFileToDirectory(file, targetDir);
                }
            }
            if (mindMaps.length > 0) { // Existing freemind-file links
                fos.write(UnicodeUtil.convert(Constants.titleMindmaps, Constants.utf8));
                for (final File mm: mindMaps) {
                    final String name = mm.getName();
                    fos.write(UnicodeUtil.convert((Constants.appletCode.replaceAll("XX", relativePath)
                            .replaceAll("YY", name)).getBytes(), Constants.utf8));
                    FileUtils.copyFileToDirectory(mm, targetDir);
                }
            }
            fos.write(UnicodeUtil.convert(Constants.footer.getBytes(), Constants.utf8));
        } catch (final IOException e) {
            if (fos != null) {
                try {
                    fos.close();
                } catch (final IOException e2) {}
            }
            e.printStackTrace();
        }
        return true;
    }
    
    /**
     * Convert one APT file.
     * @param file The file to convert
     * @param toDir The directory to write to
     */
    private static void processFile(final File file, final File toDir) {
        final String name = file.getName(); // original .apt or .txt name
        final String type = name.endsWith(Constants.markdownExtension) ?
                Constants.markdown : Constants.apt;
        System.out.println("  <" + name + ">");
        System.out.flush();
        File tmpFile = null;
        try {
            tmpFile = preprocessFile(file, type.equals(Constants.markdown));
            FileUtils.copyFileToDirectory(tmpFile, toDir);
            new File(toDir, tmpFile.getName()).renameTo(new File(toDir, name));
            tmpFile.delete();
            tmpFile = new File(toDir, name);
        } catch (final IOException e) {
            tmpFile = file;
        }
        final String realName = name.substring(0, name.lastIndexOf(Constants.dot));
        final String convertedName = realName + Constants.dot + type + Constants.xhtmlExtension;
        final String newName = realName + Constants.xhtmlExtension;
        if (type.equals(Constants.markdown)) {
            convertMarkdown(tmpFile.getAbsoluteFile(), toDir, convertedName);
            new File(toDir, newName).delete(); // delete .xhtml if existing
            new File(toDir, convertedName).renameTo(new File(toDir, newName)); // rename .mkd.xhtml to .xhtml
        } else {
            try {
                final InputFileWrapper input = InputFileWrapper.valueOf(tmpFile.getAbsolutePath(),
                        type, Constants.utf8, converter.getInputFormats());
                final OutputFileWrapper output = OutputFileWrapper.valueOf(toDir.getAbsolutePath(),
                        Constants.xhtml, Constants.utf8, converter.getOutputFormats());
                converter.convert(input, output); // convert .XX to .XX.xhtml
                new File(toDir, newName).delete(); // delete old .xhtml file if existing
                new File(toDir, convertedName).renameTo(new File(toDir, newName)); // rename .apt.xhtml to .xhtml
            } catch (final UnsupportedEncodingException e) {
                e.printStackTrace();
            } catch (final FileNotFoundException e) {
                e.printStackTrace();
            } catch (final UnsupportedFormatException e) {
                e.printStackTrace();
            } catch (final ConverterException e) {
                e.printStackTrace();
            }
        }
        if (tmpFile != file) {
            tmpFile.delete(); // only if not in original location
        }
    }
    
    /**
     * Return the first alphabetic character of the file name in lowercase.
     * Used for overview categorization in large file lists.
     * @param name The file name to check
     * @return The first lower-case character
     */
    private static String getFirstAlphaChar(final String name) {
        String value = "_";
        int i = 0;
        do {
            final String candidate = name.substring(i, i + 1).toLowerCase();
            if (candidate.matches("[a-z]")) {
                value = candidate;
                break;
            }
            i ++;
            if (i == name.length()) {
                break;
            }
        } while (true);
        return value;
    }

    /**
     * This method uses the markdown converter to convert "txt" files to whatever output we choose (currently XHTML).
     * @param inputFile The markdown txt file to convert
     * @param outputFile The output file
     */
    public static void convertMarkdown(final File inputFile, final File outputDir, final String outputName) {
        final MarkdownSinkFactory sinkFactory = new MarkdownSinkFactory() {
            @Override
            public Sink createSink(final File od, final String on, final String enc) throws IOException {
                try {
                    final File file = new File(od, on).getAbsoluteFile();
                    final Writer writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), enc));
                    writer.write("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n  \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<body>");
                    return super.createSink(writer, enc);
                } catch (final UnsupportedEncodingException e) {
                } catch (final IOException e) {
                }
                return null;
            }
        };
        try {
            final Sink sink = sinkFactory.createSink(outputDir, outputName, Constants.utf8);
            final MarkdownParser parser = new MarkdownParser();
            final Reader reader = ReaderFactory.newReader(inputFile, Constants.utf8);
            parser.parse(reader, sink);
        } catch (final FileNotFoundException e) {
            e.printStackTrace();
        } catch (final UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (final ParseException e) {
            e.printStackTrace();
        } catch (final IOException e) {
            e.printStackTrace();
        }
    }
    
    /**
     * Copies the given file to a temporary location to replace certain patterns within the file by new data. 
     * @param file The file to read
     * @param markdown If true use mkd syntax, else apt
     * @return The temporary file location
     * @throws IOException On error
     */
    private static File preprocessFile(final File file, final boolean markdown) throws IOException {
        final File newFile = File.createTempFile("_filetree_", "_filetree._tmp_", new File(System.getProperty("java.io.tmpdir")));
        newFile.deleteOnExit(); // only if program doesn't crash... stupid JRE
        final FileInputStream reader = new FileInputStream(file);
        byte[] buffer = new byte[1000];
        String toWrite = "";
        boolean stop = false;
        int last = -1;
        do {
            int length = 0;
            int b = -1;
            int inbreak = 0;
            while (last >= 0 || (b = reader.read()) >= 0) {
                if (last >= 0) {
                    b = last; // use remembered character from last outer loop
                    last = -1; // clear slot
                }
                if ((b == 10) || (b == 13)) { // if new character is read and is newline
                    if (inbreak != 0) {
                        if (inbreak == b) { // same as last time: must be 13/13 or 10/10, = two newlines
                            last = inbreak; // store for next round
                            break; // end loop
                        }
                        continue; // stay in break mode but ignore 10/13 or 13/10
                    } else { // not in break mode yet
                        inbreak = b;
                    }
                } else { // not a newline character
                    if (inbreak != 0) { // there was a newline which hasn't been written yet
                        last = b; // remember
                        inbreak = 0; // clear slot
                        break; // end loop
                    }
                    buffer[length ++] = (byte) b;
                    if (length == buffer.length) {
                    	byte[] tmp = new byte[buffer.length * 2];
                    	System.arraycopy(buffer, 0, tmp, 0, buffer.length);
                    	buffer = tmp;
                    }
                }
            }
            if (b == -1) {
                stop = true;
            }
            byte[] tmp = new byte[length];
            System.arraycopy(buffer, 0, tmp, 0, length);
            String line = new String(tmp, "UTF-8");
            if (line.isEmpty()) {
                toWrite += "\n";
                continue;
            }
            final Matcher definition = semanticDefinition.matcher(line);
            if (definition.find()) {
                final String pattern = line.substring(definition.start() + 2, definition.end() - 2);
                storePattern(pattern);
                continue;
            }
            final Matcher reference = semanticReference.matcher(line);
            if (reference.find()) {
                final String pattern = line.substring(reference.start() + 1, reference.end() - 1);
                toWrite += markdown ? writeMkdPattern(pattern) : writeAptPattern(pattern);
                continue; // TODO detect consecutive rows
            }
            toWrite += (line + "\n");
        } while (!stop);
        reader.close();
        final OutputStream fos = new FileOutputStream(newFile);
        fos.write(toWrite.getBytes(Constants.utf8));
        fos.close();
        return newFile;
    }
    
    /**
     * The pattern format: %%PLACEDATE:time,room,name%%
     * @param pattern The pattern to remember for later reference
     */
    private static void storePattern(final String pattern) {
        final String name = pattern.split(":")[0];
        patterns.put(name, pattern.split(":")[1].trim().replace("|", "\\|"));
        counts.put(name, new AtomicInteger(0));
    }
    
    /**
     * Simple join.
     * @param sep Separator null or "" or any string
     * @param strings the strings to join
     * @return The joined string
     */
    static String join(final String sep, final String[] strings) {
    	final StringBuilder sb = new StringBuilder();
    	for (int i = 0; i < strings.length - 1; i ++) {
    		sb.append(strings[i]);
    		sb.append(sep == null ? "" : sep);
    	}
    	if (strings.length > 1) {
    		sb.append(strings[strings.length - 1]);
    	}
    	return sb.toString();
    }
    
    /**
     * Copy an array range.
     * @param args String array
     * @param first The first index, 0-based or negative for end of array offset
     * @param lastPlusOne The last index plus one or 0 (all), 0-based or negative for end of array offset
     * @return The new array
     */
    static String[] arrayRange(final String[] args, int first, int lastPlusOne) {
    	if (lastPlusOne == 0) {
    		lastPlusOne = args.length;
    	} else if (lastPlusOne < 0) {
    		lastPlusOne += args.length;
    	}
    	if (first < 0) {
    		first += args.length;
    	}
    	if ((first < 0) || (first > args.length) || (lastPlusOne < 0) || (lastPlusOne > args.length) || (first > lastPlusOne)) {
    		throw new IllegalArgumentException();
    	}
    	final String[] ret = new String[lastPlusOne - first];
    	for (int i = 0; i < lastPlusOne - first; i ++) {
    		ret[i] = args[i + first];
    	}
    	return ret;
    }
    
    /**
     * Pattern is already pruned of leading !.
     * @param pattern
     * @return
     * @throws IOException
     */
    private static String writeAptShortPattern(final String pattern) throws IOException {
        final String name = pattern.split(":")[0]; // get name of pattern to use
        final String pat = patterns.get(name);
        if (pat == null) {
            System.err.println("** Referenced pattern could not be found: '" + name + "'");
            return "";
        }
        final String[] def = pat.split(";");
        final String[] data = join(":", arrayRange(pattern.split(":"), 1, 0)).trim().replace("|", "\\|").split(";");
        
        String contents = "\n |<" + name + " " + counts.get(name).incrementAndGet() + ">| ";
        for (final String key: def) {
            for (final String entry: data) {
                if (entry.startsWith(key)) {
                	contents += "<<" + key + ">> ";
                    contents += entry.split("=")[1]; // couldn't work  .replace("\r\n"," ").replace("\n"," ").replace("\r"," ");
                    contents += "| ";
                }
            }
        }
        return contents + "\n";
    }
    
    /**
     * Write out a semantic data according to the pattern defined earlier.
     * @param pattern The extracted pattern string %patternName: room=HS2;name=Arne Bachmann;time=12:30%
     * @return The contents to write
     * @throws IOException On error
     */
    private static String writeAptPattern(final String pattern) throws IOException {
    	if (pattern.startsWith("!")) {
    		return writeAptShortPattern(pattern.substring(1));
    	}
        final String name = pattern.split(":")[0]; // get name of pattern to use
        final String pat = patterns.get(name);
        if (pat == null) {
            System.err.println("** Referenced pattern could not be found: '" + name + "'");
            return "";
        }
        final String[] def = pat.split(";");
        final String[] data = join(":", arrayRange(pattern.split(":"), 1, 0)).trim().replace("|", "\\|").split(";");
        
        String header = "";
        for (final String entry: def) { // add header
            header += "| <<";
            header += entry.split("=")[0];
            header += ">>";
        }
        header += " |\n";
        
        String contents = "| ";
        int count = 0;
        for (final String key: def) {
            for (final String entry: data) {
                if (entry.startsWith(key)) {
                    count ++;
                    contents += entry.split("=")[1]; // couldn't work  .replace("\r\n"," ").replace("\n"," ").replace("\r"," ");
                    contents += " | ";
                }
            }
        }
        contents += "\n";
        
        String separator = "";
        separator += "*--"; // * is first mark, no meaning
        for (int i = 1; i < count; i ++) {
            separator += "-+"; // + means left-align of column left of it
        }
        separator += "-+\n"; // * would mean centering
        
        return "\n" + separator.replace("+", "*") + header + separator + contents + separator + name + " " + counts.get(name).incrementAndGet() + "\n";
    }
    
    /**
     * @param pattern
     * @return
     * @throws IOException
     */
    private static String writeMkdPattern(final String pattern) throws IOException {
        final String name = pattern.split(":")[0]; // get name of pattern to use
        final String pat = patterns.get(name);
        if (pat == null) {
            System.err.println("** Referenced pattern could not be found: '" + name + "'");
            return "";
        }
        final String[] def = pat.split(";");
        final String[] data = join(":", arrayRange(pattern.split(":"), 1, 0)).trim().replace("|", "\\|").split(";");
        
        String header = "\n<table border=\"1\">\n    <caption>" + name + " " + counts.get(name).incrementAndGet() + "</caption>\n    <tr>\n";
        for (final String entry: data) { // add header
            header += "        <th>";
            header += entry.split("=")[0];
            header += "</th>\n";
        }
        header += "    </tr>\n";

        String contents = "    <tr>\n";
        for (final String key: def) {
            for (final String entry: data) {
                if (entry.startsWith(key)) {
                    contents += "        <td>";
                    contents += entry.split("=")[1];// could never work .replace("\r\n","<br/>").replace("\n","<br/>").replace("\r","<br/>");
                    contents += "</td>\n";
                }
            }
        }
        contents += "    </tr>\n</table>\n";
        return header + contents;
    }
    
}
